#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <asoundlib.h>

#include "au.h"
#include "adev.h"
#include "common.h"

enum {
	A_NULL,
	A_ENC,
	A_RATE,
	A_CHAN
};

struct adev {
	snd_pcm_t		*snd;
	snd_pcm_hw_params_t	*param;
	size_t			 ssize;
	uint32_t		 enc;
	unsigned		 chan;
	int			 mode;
};

const struct a_lookup stab[] = {
	{ "enc", A_ENC },
	{ "encoding", A_ENC },
	{ "rate", A_RATE },
	{ "chan", A_CHAN },
	{ "channel", A_CHAN },
	{ "channels", A_CHAN },
	{ NULL, A_NULL }
};

static snd_pcm_format_t	auenc2sndpcmfmt(uint32_t);
static uint32_t		sndpcmfmt2auenc(snd_pcm_format_t);

struct adev *
ad_open(const char *dev, int mode)
{
	struct adev *a;
	snd_pcm_format_t fmt = SND_PCM_FORMAT_UNKNOWN;
	unsigned int rate = 48000;
	int e;

	if (dev == NULL)
		dev = "default";
	if ((a = malloc(sizeof(struct adev))) == NULL)
		return (NULL);
	a->enc = AU_PCM16;
	a->chan = 2;
	a->mode = mode == A_READ;
	if ((e = snd_pcm_open(&a->snd, dev, a->mode ?
	    SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
		free(a);
		errno = -e;
		return (NULL);
	}
	if ((e = snd_pcm_hw_params_malloc(&a->param)) < 0) {
		snd_pcm_close(a->snd);
		free(a);
		errno = -e;
		return (NULL);
	}
	if ((e = snd_pcm_hw_params_any(a->snd, a->param)) < 0)
		goto err;
	if ((e = snd_pcm_hw_params_set_access(a->snd, a->param,
	    SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
		goto err;
	if ((e = snd_pcm_hw_params_set_format(a->snd, a->param,
	    auenc2sndpcmfmt(a->enc))) < 0)
		goto err;
	if ((e = snd_pcm_hw_params_set_channels_near(a->snd, a->param,
	    &a->chan)) < 0)
		goto err;
	if ((e = snd_pcm_hw_params_set_rate_near(a->snd, a->param,
	    &rate, 0)) < 0)
		goto err;
	a->ssize = au_encsize(a->enc) * (size_t)a->chan;
	return (a);
err:
	ad_close(a);
	errno = -e;
	return (NULL);
}

struct adev *
ad_fpopen(FILE *fp, int mode)
{

	ERR(EOPNOTSUPP, NULL);
}

void
ad_close(struct adev *a)
{

	if (a == NULL)
		return;
	(void)snd_pcm_drain(a->snd);
	snd_pcm_hw_params_free(a->param);
	snd_pcm_close(a->snd);
	free(a);
}

int
ad_limit(struct adev *a)
{

	ERR(ENOSYS, -1);
}

int
ad_get(struct adev *a, const char *s, ...)
{
	va_list ap;
	ssize_t r;
	unsigned u;
	int n;
	int v;
	int i;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_ENC:
			*va_arg(ap, uint32_t *) = a->enc;
			break;
		case A_RATE:
			if (snd_pcm_hw_params_get_rate(a->param, &u, &i)
			    < 0)
				goto err;
			if (u > UINT32_MAX)
				errno = EINVAL;
			*va_arg(ap, uint32_t *) = u;
			break;
		case A_CHAN:
			*va_arg(ap, uint32_t *) = a->chan;
			break;
		default:
			goto err;
		}
	if (r == 0) {
		va_end(ap);
		return (n);
	}
err:
	va_end(ap);
	ERR(EINVAL, n);
}

int
ad_set(struct adev *a, const char *s, ...)
{
	va_list ap;
	ssize_t r;
	uint32_t u32;
	int n;
	int v;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_ENC:
			if ((u32 = va_arg(ap, uint32_t)) > UINT_MAX)
				goto err;
			if (snd_pcm_hw_params_set_format(a->snd,
			    a->param, auenc2sndpcmfmt(u32)) < 0)
				goto err;
			(void)snd_pcm_drain(a->snd);
			if (snd_pcm_hw_params(a->snd, a->param) < 0)
				goto err;
			a->enc = u32;
			break;
		case A_RATE:
			if ((u32 = va_arg(ap, uint32_t)) > UINT_MAX)
				goto err;
			if (snd_pcm_hw_params_set_rate(a->snd, a->param,
			    u32, 0) < 0)
				goto err;
			(void)snd_pcm_drain(a->snd);
			if (snd_pcm_hw_params(a->snd, a->param) < 0)
				goto err;
			break;
		case A_CHAN:
			if ((u32 = va_arg(ap, uint32_t)) > UINT_MAX)
				goto err;
			if (snd_pcm_hw_params_set_channels(a->snd,
			    a->param, u32) < 0)
				goto err;
			(void)snd_pcm_drain(a->snd);
			if (snd_pcm_hw_params(a->snd, a->param) < 0)
				goto err;
			a->chan = u32;
			break;
		default:
			goto err;
		}
	if (r == 0) {
		va_end(ap);
		return (n);
	}
err:
	va_end(ap);
	ERR(EINVAL, n);
}

ssize_t
ad_read(struct adev *a, void *b, size_t s)
{
	ssize_t r;

	if (a == NULL || b == NULL || !a->mode)
		ERR(EINVAL, -1);
	s /= a->ssize;
	if (s == 0)
		return (0);
	r = snd_pcm_readi(a->snd, b, s);
	return (r > 0 ? r * a->ssize : r);
}

ssize_t
ad_write(struct adev *a, const void *b, size_t s)
{
	ssize_t r;

	if (a == NULL || b == NULL || a->mode)
		ERR(EINVAL, -1);
	s /= a->ssize;
	if (s == 0)
		return (0);
	r = snd_pcm_writei(a->snd, b, s);
	return (r > 0 ? r * a->ssize : r);
}

snd_pcm_format_t
auenc2sndpcmfmt(uint32_t enc)
{

	switch (enc) {
	case AU_ULAW:
		return (SND_PCM_FORMAT_MU_LAW);
	case AU_PCM8:
		return (SND_PCM_FORMAT_S8);
	case AU_PCM16:
		return (SND_PCM_FORMAT_S16_BE);
	case AU_PCM24:
		return (SND_PCM_FORMAT_S24_3BE);
	case AU_PCM32:
		return (SND_PCM_FORMAT_S32_BE);
	case AU_FLOAT32:
		return (SND_PCM_FORMAT_FLOAT_BE);
	case AU_FLOAT64:
		return (SND_PCM_FORMAT_FLOAT64_BE);
	case AU_ALAW:
		return (SND_PCM_FORMAT_A_LAW);
	default:
		return (SND_PCM_FORMAT_S16_BE);
	}
}

uint32_t
sndpcmfmt2auenc(snd_pcm_format_t fmt)
{

	switch (fmt) {
	case SND_PCM_FORMAT_S8:
		return (AU_PCM8);
	case SND_PCM_FORMAT_U8:
		return (AU_PCM8);
	case SND_PCM_FORMAT_S16_LE:
		return (AU_PCM16);
	case SND_PCM_FORMAT_U16_LE:
		return (AU_PCM16);
	case SND_PCM_FORMAT_S16_BE:
		return (AU_PCM16);
	case SND_PCM_FORMAT_U16_BE:
		return (AU_PCM16);
	case SND_PCM_FORMAT_S24_LE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_U24_LE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_S24_BE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_U24_BE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_S32_LE:
		return (AU_PCM32);
	case SND_PCM_FORMAT_U32_LE:
		return (AU_PCM32);
	case SND_PCM_FORMAT_S32_BE:
		return (AU_PCM32);
	case SND_PCM_FORMAT_U32_BE:
		return (AU_PCM32);
	case SND_PCM_FORMAT_FLOAT_LE:
		return (AU_FLOAT32);
	case SND_PCM_FORMAT_FLOAT64_LE:
		return (AU_FLOAT64);
	case SND_PCM_FORMAT_FLOAT_BE:
		return (AU_FLOAT32);
	case SND_PCM_FORMAT_FLOAT64_BE:
		return (AU_FLOAT64);
	case SND_PCM_FORMAT_MU_LAW:
		return (AU_ULAW);
	case SND_PCM_FORMAT_A_LAW:
		return (AU_ALAW);
	case SND_PCM_FORMAT_IMA_ADPCM:
		return (AU_PCM16);
	case SND_PCM_FORMAT_S24_3LE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_U24_3LE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_S24_3BE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_U24_3BE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_S20_3LE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_U20_3LE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_S20_3BE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_U20_3BE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_S18_3LE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_U18_3LE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_S18_3BE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_U18_3BE:
		return (AU_PCM24);
	case SND_PCM_FORMAT_G723_24:
		return (AU_PCM16);
	case SND_PCM_FORMAT_G723_24_1B:
		return (AU_PCM16);
	case SND_PCM_FORMAT_G723_40:
		return (AU_PCM16);
	case SND_PCM_FORMAT_G723_40_1B:
		return (AU_PCM16);
	case SND_PCM_FORMAT_DSD_U8:
		return (AU_PCM8);
	case SND_PCM_FORMAT_DSD_U16_LE:
		return (AU_PCM16);
	case SND_PCM_FORMAT_DSD_U32_LE:
		return (AU_PCM32);
	case SND_PCM_FORMAT_DSD_U16_BE:
		return (AU_PCM16);
	case SND_PCM_FORMAT_DSD_U32_BE:
		return (AU_PCM32);
	default:
		return (AU_PCM16);
	}
}
